/**
 * ©2013-2015 Alan L. Rights Reserved.
 */
package org.freedom.railhunter.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
import org.apache.http.NoHttpResponseException;
import org.apache.http.ParseException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.entity.GzipDecompressingEntity;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.freedom.railhunter.model.AcceptDataType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Alan
 */
public class HttpConnectionManager {
    
    // 连接池里的最大连接数 
    public static final int MAX_TOTAL_CONNECTIONS = 100;
    
    // 每个路由的默认最大连接数  
    public static final int MAX_ROUTE_CONNECTIONS = 50;
    
    // 连接超时时间 
    public static final int CONNECT_TIMEOUT = 50000;
    
    // 套接字超时时间
    public static final int SOCKET_TIMEOUT = 50000;
    
    // 连接池中 连接请求执行被阻塞的超时时间
    public static final long CONN_MANAGER_TIMEOUT = 60000;
    
    // http连接相关参数
    private static final HttpParams parentParams;
    
    // http线程池管理器
    private static final PoolingClientConnectionManager cm;
    
    // http客户端
    private static final DefaultHttpClient httpClient;
    
    // HTTP头
    public static String HEAD_X_REQUESTED_WITH = "X-Requested-With";
    
    private static final Logger logger = LoggerFactory.getLogger(HttpConnectionManager.class);
    
    private static final X509TrustManager tm = new X509TrustManager() {
        @Override
        public void checkClientTrusted(java.security.cert.X509Certificate[] xcs, String string)
                throws java.security.cert.CertificateException {
        }

        @Override
        public void checkServerTrusted(java.security.cert.X509Certificate[] xcs, String string)
                throws java.security.cert.CertificateException {
        }

        @Override
        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    };
    
    /**
     * 初始化
     */
    static {
        try {
            SSLContext sslcontext = SSLContext.getInstance("TLS");
            sslcontext.init(null, new TrustManager[]{tm}, null);
            SSLSocketFactory ssf = new SSLSocketFactory(sslcontext);
            SchemeRegistry sr = new SchemeRegistry();
            sr.register(new Scheme("https", 443, ssf));
            cm = new PoolingClientConnectionManager(sr);
            cm.setMaxTotal(MAX_TOTAL_CONNECTIONS);
            cm.setDefaultMaxPerRoute(MAX_ROUTE_CONNECTIONS);
            
            parentParams = new BasicHttpParams();
            parentParams.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
            parentParams.setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, "UTF-8");
            parentParams.setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
            parentParams.setParameter(ClientPNames.CONN_MANAGER_TIMEOUT, CONN_MANAGER_TIMEOUT);
            parentParams.setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECT_TIMEOUT);
            parentParams.setParameter(CoreConnectionPNames.SO_TIMEOUT, SOCKET_TIMEOUT);
            parentParams.setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
            parentParams.setParameter(ClientPNames.HANDLE_REDIRECTS, true);
            
            //请求重试处理
            HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {
                @Override
                public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
                    if (executionCount >= 5) {
                        // 如果超过最大重试次数，那么就不要继续了
                        return false;
                    }
                    if (exception instanceof NoHttpResponseException) {
                        // 如果服务器丢掉了连接，那么就重试
                        return true;
                    }
                    if (exception instanceof SSLHandshakeException) {
                        // 不要重试SSL握手异常
                        return false;
                    }
                    HttpRequest request = (HttpRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST);
                    boolean idempotent = !(request instanceof HttpEntityEnclosingRequest);
                    if (idempotent) {
                        // 如果请求被认为是幂等的，那么就重试
                        return true;
                    }
                    return false;
                }
            };
        
            httpClient = new DefaultHttpClient(cm, parentParams);
            httpClient.setHttpRequestRetryHandler(httpRequestRetryHandler);
        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new IllegalStateException(e);
        }
    }
    
    /**
     * GET请求
     *
     * @param httpClient
     * @param url
     * @param parameters
     * @param cookieData
     * @return
     */
    public static HttpResponse getHttpRequest(String url, String referer,
            List<NameValuePair> parameters, AcceptDataType acceptDataType, Map<String, String> cookieData) throws IOException {
        logger.debug("------------------------------------------------------------------------");
        if (parameters != null && parameters.size() > 0) {
            String paramURL = URLEncodedUtils.format(parameters, HTTP.UTF_8);
            if (url.indexOf("?") > -1) {
                url = url + "&" + paramURL;
            } else {
                url = url + "?" + paramURL;
            }
        }

        logger.debug("GET URL: " + url);

        HttpGet httpGet = new HttpGet(url);

        // set header
        httpGet.setHeader(HttpHeaders.ACCEPT, acceptDataType.getTypeStr());
        httpGet.setHeader(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate");
        httpGet.setHeader(HttpHeaders.ACCEPT_LANGUAGE, "zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3");
        httpGet.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache");
        httpGet.setHeader(HttpHeaders.CONNECTION, "keep-alive");
        httpGet.setHeader(HttpHeaders.HOST, "kyfw.12306.cn");
        httpGet.setHeader(HttpHeaders.REFERER, referer);
        httpGet.setHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 5.1; rv:26.0) Gecko/20100101 Firefox/26.0");
        
        if (cookieData != null) {
            boolean first = true;
            StringBuilder cookie = new StringBuilder();
            for (Map.Entry<String, String> me : cookieData.entrySet()) {
                if (first) {
                    first = false;
                } else {
                    cookie.append("; ");
                }
                cookie.append(me.getKey()).append("=").append(me.getValue());
            }
            httpGet.setHeader("Cookie", cookie.toString());
        }

        if (logger.isDebugEnabled()) {
            if (parameters != null) {
                logger.debug(" + Request parameters: ");
                for (NameValuePair param : parameters) {
                    logger.debug("   - " + param.getName() + " : " + param.getValue());
                }
            }
            logger.debug(" + Request headers: ");
            for (Header header : httpGet.getAllHeaders()) {
                logger.debug("   - " + header.getName() + " : " + header.getValue());
            }
        }

        HttpResponse response = httpClient.execute(httpGet);
        if (logger.isDebugEnabled()) {
            logger.debug(" + Response headers: ");
            for (Header header : response.getAllHeaders()) {
                logger.debug("   - " + header.getName() + " : " + header.getValue());
            }
        }
        logger.debug("***********************************************************************");
        return response;
    }

    /**
     * 返回GET请求响应字符串
     *
     * @param httpClient
     * @param url
     * @param parameters
     * @param cookieData
     * @return
     */
    public static String getHttpRequestAsString(String url, String referer,
            List<NameValuePair> parameters, Map<String, String> cookieData) throws IOException {
        HttpResponse response = getHttpRequest(url, referer, parameters, AcceptDataType.All, cookieData);
        HttpEntity entity = response.getEntity();
        String responseContent = readContentFromEntity(entity);
        logger.info("GET: " + url);
        if (responseContent.length() > 300) {
            logger.info(" + Response content(0-300):\n" + responseContent.substring(0, 100));
        } else {
            logger.info(" + Response content:\n" + responseContent);
        }
        //logger.debug(" + Response content (ALL):\n" + responseContent);
        return responseContent;
    }
    
    /**
     * 新建一个HttpPost对象
     * 
     * @param url
     * @param referer
     * @param acceptDataType
     * @param parameters
     * @param cookieData
     * @return
     * @throws UnsupportedEncodingException 
     */
    public static HttpPost createHttpPost(String url, String referer, AcceptDataType acceptDataType,
            List<NameValuePair> parameters, String content, Map<String, String> cookieData) throws IOException {
        
        HttpPost httpPost = new HttpPost(url);
        
        // set header
        httpPost.setHeader(HttpHeaders.ACCEPT, acceptDataType.getTypeStr());
        httpPost.setHeader(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate");
        httpPost.setHeader(HttpHeaders.ACCEPT_LANGUAGE, "zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3");
        httpPost.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache");
        httpPost.setHeader(HttpHeaders.CONNECTION, "keep-alive");
        httpPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded; charset=UTF-8");
        httpPost.setHeader(HttpHeaders.HOST, "kyfw.12306.cn");
        httpPost.setHeader(HttpHeaders.PRAGMA, "no-cache");
        httpPost.setHeader(HttpHeaders.REFERER, referer);
        httpPost.setHeader(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 5.1; rv:26.0) Gecko/20100101 Firefox/26.0");
        httpPost.setHeader(HEAD_X_REQUESTED_WITH, "XMLHttpRequest");

        if (cookieData != null) {
            boolean first = true;
            StringBuilder cookie = new StringBuilder();
            for (Map.Entry<String, String> me : cookieData.entrySet()) {
                if (first) {
                    first = false;
                } else {
                    cookie.append("; ");
                }
                cookie.append(me.getKey()).append("=").append(me.getValue());
            }
            httpPost.setHeader("Cookie", cookie.toString());
        }

        if (parameters != null) {
            if (StringUtils.isEmpty(content)) {
                UrlEncodedFormEntity uef = new UrlEncodedFormEntity(parameters, "UTF-8");
                httpPost.setEntity(uef);
            } else {
                StringEntity se = new StringEntity(content, ContentType.create(URLEncodedUtils.CONTENT_TYPE, "UTF-8"));
                //logger.debug("Content source: " + readContentFromEntity(se) + "，content length: " + se.getContentLength());
                httpPost.setEntity(se);
            }
        }
        
        return httpPost;
    }
    
    /**
     * 从流中将字符串读出
     * 
     * @param is
     * @return
     * @throws IOException 
     */
    public static String readStringFromInputStream(InputStream is) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i = -1;
        while ((i = is.read()) != -1) {
            baos.write(i);
        }
        return baos.toString();
    }

    /**
     * POST请求
     *
     * @param httpclient
     * @param url
     * @param parameters
     * @param cookieData
     * @return
     */
    public static HttpResponse postHttpRequest(String url, String referer, AcceptDataType acceptDataType,
            List<NameValuePair> parameters, String content, Map<String, String> cookieData) throws IOException {
        logger.debug("------------------------------------------------------------------------");
        logger.debug("POST URL: " + url);

        HttpPost httpPost = createHttpPost(url, referer, acceptDataType, parameters, content, cookieData);
        
        if (logger.isDebugEnabled()) {
            if (parameters != null) {
                logger.debug(" + Request parameters: ");

                for (NameValuePair param : parameters) {
                    logger.debug("   - " + param.getName() + " : " + param.getValue());
                }
            }
            logger.debug(" + Request headers: ");
            for (Header header : httpPost.getAllHeaders()) {
                logger.debug("   - " + header.getName() + " : " + header.getValue());
            }
        }

        HttpResponse response = httpClient.execute(httpPost);
        if (logger.isDebugEnabled()) {
            logger.debug(" + Response headers: ");
            for (Header header : response.getAllHeaders()) {
                logger.debug("   - " + header.getName() + " : " + header.getValue());
            }
        }
        logger.debug("***********************************************************************");
        return response;
    }
    
    /**
     * 返回POST请求响应字符串
     *
     * @param httpClient
     * @param url
     * @param parameters
     * @param cookieData
     * @return
     */
    public static String postHttpRequestAsString(String url, String referer, AcceptDataType acceptDataType,
            List<NameValuePair> parameters, Map<String, String> cookieData) throws IOException {
        HttpResponse response = postHttpRequest(url, referer, acceptDataType, parameters, null, cookieData);
        HttpEntity entity = response.getEntity();
        String responseContent = readContentFromEntity(entity);
        logger.info("POST: " + url);
        if (responseContent.length() > 300) {
            logger.info(" + Response content(0-300):\n" + responseContent.substring(0, 100));
        } else {
            logger.info(" + Response content:\n" + responseContent);
        }
        //logger.debug(" + Response content(ALL):\n" + responseContent);
        return responseContent;
    }
    
    /**
     * 返回POST请求响应字符串
     * 
     * @param url
     * @param referer
     * @param acceptDataType
     * @param parameters
     * @param encode                 是否要encode
     * @param cookieData
     * @return
     * @throws IOException 
     */
    public static String postHttpRequestAsString(String url, String referer, AcceptDataType acceptDataType,
            List<NameValuePair> parameters, String content, Map<String, String> cookieData) throws IOException {
        HttpResponse response = postHttpRequest(url, referer, acceptDataType, parameters, content, cookieData);
        HttpEntity entity = response.getEntity();
        String responseContent = readContentFromEntity(entity);
        logger.info("POST: " + url);
        if (responseContent.length() > 300) {
            logger.info(" + Response content(0-300):\n" + responseContent.substring(0, 100));
        } else {
            logger.info(" + Response content:\n" + responseContent);
        }
        //logger.debug(" + Response content(ALL):\n" + responseContent);
        return responseContent;
    }

    /**
     * 从response返回的实体中读取页面代码
     * 
     * @param httpEntity Http实体
     * @return 页面代码
     * @throws ParseException
     * @throws IOException
     */
    private static String readContentFromEntity(HttpEntity httpEntity) throws ParseException, IOException {
        String html;
        Header header = httpEntity.getContentEncoding();
        if (header != null && "gzip".equals(header.getValue())) {
            html = EntityUtils.toString(new GzipDecompressingEntity(httpEntity));
        } else {
            html = EntityUtils.toString(httpEntity);
        }
        return html;
    }
}
